﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using YAPL.Language.Ast;
using YAPL.Util;

namespace YAPL.Language.Ast.Utils {
	public class DefaultAstWalker : IAstWalker {
		private Expression _current;
		private readonly Expression _root;
		private readonly AstWalkType _walkType;
		private readonly HashSet<Guid> _visitedNodes = new HashSet<Guid>();
		private readonly Stack<Expression> _walkStack = new Stack<Expression>();
		private bool _nextNodeReached;

		public DefaultAstWalker(Expression root, AstWalkType walkType) {
			Require.NotNull(root);
			_root = root;
			_walkType = walkType;
			_current = null;
		}

		public DefaultAstWalker(Expression root) : this(root, AstWalkType.PreOrder) {}

		/// <summary>
		/// Current Ast node
		/// <returns>current node or Expressions.Undefined, if just initialized or end of tree reached</returns>
		/// </summary>
		public Expression Current {
			get {
				return _current ?? Expressions.Undefined();
			}
		}

		/// <summary>
		/// noves to next node
		/// </summary>
		/// <returns>true if next node exists</returns>
		public bool MoveNext() {
			_nextNodeReached = false;
			if (_current == null) _current = _root;
			var props = _current.GetType().GetProperties();

			var comparer = new DescendantAttributePriorityComparer();
			var descendantsBeforeCurrent = new SortedDictionary<DescendantAttribute, PropertyInfo>(comparer);
			var descendantsAfterCurrent = new SortedDictionary<DescendantAttribute, PropertyInfo>(comparer);
			foreach (var prop in props) {
				var descendantAttrs = (DescendantAttribute[]) prop.GetCustomAttributes(typeof (DescendantAttribute), false);
				if (descendantAttrs.Length == 0) continue;

				if (/* descendantAttrs[0].Visit == AstVisit.BeforeCurrent && */_walkType == AstWalkType.PostOrder)
					descendantsBeforeCurrent.Add(descendantAttrs[0],prop);
				descendantsAfterCurrent.Add(descendantAttrs[0], prop);
			}

			GoToDescendants(descendantsBeforeCurrent);
			if (_nextNodeReached)
				return true;

			if (!_visitedNodes.Contains(_current.ID)) {
				_visitedNodes.Add(_current.ID);
				_nextNodeReached = true;
				return true;
			}

			GoToDescendants(descendantsAfterCurrent);
			if (_nextNodeReached)
				return true;

			if (_walkStack.Count == 0) {
				_current = null;
				return false;
			}

			//return to parent and invoke 'MoveNext' recursively
			_current = _walkStack.Pop();
			return MoveNext();
		}

		public void Reset() {
			_walkStack.Clear();
			_visitedNodes.Clear();
			_current = null;
		}


		public void RewriteCurrent(Expression newExpr) {
			var oldExpr = Current;
			if (!ReferenceEquals(newExpr, oldExpr)) {
				//if has parent, update references
				if (_walkStack.Count > 0) {
					var parent = _walkStack.Peek();
					parent.RewriteChild(oldExpr, newExpr);
				}
				_current = newExpr;
				if (!_visitedNodes.Contains(_current.ID)) {
					_visitedNodes.Add(_current.ID);
				}
			}
		}


		private void GoToDescendants(SortedDictionary<DescendantAttribute, PropertyInfo> descendants) {
			foreach (var pair in descendants) {
				var prop = pair.Value;
				var exprList = GetExpressionsFromProperty(prop);

				foreach (var descendant in exprList) {
					if (_visitedNodes.Contains(descendant.ID)) continue;
					_walkStack.Push(_current);
					_current = descendant;
					MoveNext();
					if (_nextNodeReached)
						return;
				}
			}
		}

		private IList<Expression> GetExpressionsFromProperty(PropertyInfo prop) {
			var exprList = new List<Expression>();
			var getMethod = prop.GetGetMethod();
			/*if (getMethod.ReturnType.IsAssignableFrom(typeof(IEnumerable))) {
				var exprs = (IEnumerable<Expression>)getMethod.Invoke(_current, new object[] { });
				exprList.AddRange(exprs);
			}*/
			if (typeof(Expression).IsAssignableFrom(getMethod.ReturnType)) {
				var iResult = getMethod.Invoke(_current, new object[] { });
				if (iResult != null) {
					var expr = (Expression)iResult;
					exprList.Add(expr);
				}
			} else {
				try {
					var iResult = getMethod.Invoke(_current, new object[] { });
					if (iResult != null) {
						var exprs = (IEnumerable)iResult;
						exprs.Each(expr => exprList.Add((Expression)expr));
					}
				}
				catch (Exception) {
					throw new InvalidDescendantException(prop);
				}
			}
			return exprList;
		}
	}
}
